"""Profraw manager"""

# pylint: disable=C0301:line-too-long

from os import walk, makedirs
from os.path import exists, join, basename
from typing import List

from loguru import logger

from ios_cov_manager.lib.cmd_util import execute_command
from ios_cov_manager.lib.string_util import (
    parse_device_profraw_file_name,
)
from ios_cov_manager.lib.system_util import (
    find_file_in_directory,
    get_filename_and_dir,
)


class ProfrawError(Exception):
    """Exception raised for profraw manager errors"""

    def __init__(self, message):
        super().__init__(message)
        self.message = message

    def __str__(self):
        return self.message


class ProfrawUtil:
    """Profraw Manager"""

    def __init__(self):
        pass

    @staticmethod
    def merge_profraws_into_html(
        paths: List[str],
        output_dir_path: str,
        macho_file_path: str,
        build_machine_repo_path: str,
        coverage_machine_repo_path: str,
    ):
        """Merge multiple profraw files into an HTML report file

        Args:
        paths (list[str]): The paths to the profraw files, the profraw files should relate to the same macho file
        output_dir_path (str): The directory where the merged profdata should be stored
        """

        macho_file_name = basename(macho_file_path)

        # Merge all the profraw files into one profdata file
        # The paths may be very long, so we need to use the `-cwd` option in order to avoid `the command is too long``
        # The profraw files are all must under the same directory, so we can use the `-cwd` option
        _, _error_lines = execute_command(
            command=f"xcrun llvm-profdata merge -sparse {paths} -o {output_dir_path}/{macho_file_name}.profdata",
        )
        if _error_lines:
            raise ProfrawError("Failed to merge profraw files")

        # Export the profdata file to a text file
        _, _error_lines = execute_command(
            command=f"xcrun llvm-cov export {macho_file_path} -instr-profile={output_dir_path}/{macho_file_name}.profdata -format=text -summary-only > {output_dir_path}/{macho_file_name}_profdata.txt"
        )
        if _error_lines:
            raise ProfrawError("Failed to export profraw files into a text file")

        # Generate the html report file
        # -path-equivalence: https://github.com/llvm/llvm-project/blob/main/llvm/tools/llvm-cov/CodeCoverage.cpp
        path_equivalence = f"{build_machine_repo_path},{coverage_machine_repo_path}"
        logger.info(
            f"Generating html report file, the path equivalence is set: {path_equivalence}"
        )
        _, _error_lines = execute_command(
            command=f'xcrun llvm-cov show {macho_file_path} -instr-profile={output_dir_path}/{macho_file_name}.profdata -use-color -format=html -output-dir={output_dir_path}/{macho_file_name} -path-equivalence="{path_equivalence}"'
        )

    @staticmethod
    def parse_profraw(
        profraw_file_path: str,
        macho_file_path: str,
        output_dir: str,
        build_machine_repo_path: str,
        coverage_machine_repo_path: str,
    ):
        """Parse the profraw on the same branch

        # Creating coverage reports
        # https://clang.llvm.org/docs/SourceBasedCodeCoverage.html

        Args:
        profraw_path (str): The path to the profraw file
        output_dir (str): The directory where the output files will be stored

        Returns:
        List[str]: A list of error lines
        """

        # Get the id of the device and the name of the macho file
        _, macho_file_name = parse_device_profraw_file_name(
            file_name=basename(profraw_file_path),
        )

        # Generate the profdata.data file
        if exists(f"{output_dir}/{macho_file_name}.profdata"):
            execute_command(
                command=f"xcrun llvm-profdata merge -sparse {profraw_file_path} {output_dir}/{macho_file_name}.profdata -o {output_dir}/{macho_file_name}.profdata"
            )
        else:
            execute_command(
                command=f"xcrun llvm-profdata merge -sparse {profraw_file_path} -o {output_dir}/{macho_file_name}.profdata"
            )

        # Generate the profdata.info file
        execute_command(
            command=f"xcrun llvm-profdata show -all-functions -instr {output_dir}/{macho_file_name}.profdata -output={output_dir}/profdata.info"
        )

        # Export the profdata file to a text file
        execute_command(
            command=f"xcrun llvm-cov export {macho_file_path} -instr-profile={output_dir}/{macho_file_name}.profdata -format=text -summary-only > {output_dir}/{macho_file_name}_profdata.txt"
        )

        # Generate the profdata.lcov file
        _, error_lines = execute_command(
            command=f"xcrun llvm-cov export {macho_file_path} -instr-profile={output_dir}/{macho_file_name}.profdata -format=lcov > {output_dir}/profdata.lcov",
            console_output=False,
        )
        if error_lines:
            for line in error_lines:
                if "warning:" in line:
                    continue

                logger.error(f"Execute command failed: {line}")
                raise RuntimeError
        logger.success(f"Successfully generated lcov file: {output_dir}/profdata.lcov")

        # Generate the html report file
        # -path-equivalence: https://github.com/llvm/llvm-project/blob/main/llvm/tools/llvm-cov/CodeCoverage.cpp
        path_equivalence = f"{build_machine_repo_path},{coverage_machine_repo_path}"
        logger.info(
            f"Generating html report file, the path equivalence is set: {path_equivalence}"
        )
        _, error_lines = execute_command(
            command=f'xcrun llvm-cov show {macho_file_path} -instr-profile={output_dir}/{macho_file_name}.profdata -use-color -format=html -output-dir={output_dir}/{macho_file_name} -path-equivalence="{path_equivalence}"'
        )
        return error_lines

    @staticmethod
    def supplement_file(file_path: list, ios_repo_dir: str, additional_dir: str):
        """Search the file from the repository and the additional directory and copy it to the output directory

        You can put the file which can not be found in the ios repo to the additional directory in order to make the coverage report more accurate
        """

        filename, dirpath = get_filename_and_dir(file_path)
        if not exists(dirpath):
            logger.warning(
                f"Directory of the file {filename}({file_path}) not exists: {dirpath}, trying to create it"
            )
            makedirs(dirpath)
        found_file_path = ""

        # Iterate over all files in the derived_sources directory
        derived_sources_filenames = []
        for _, _, files in walk(additional_dir):
            for file in files:
                derived_sources_filenames.append(file)
        if filename in derived_sources_filenames:
            found_file_path = find_file_in_directory(additional_dir, filename)
            if not found_file_path:
                logger.error(
                    f"Can not find the file {filename} under the directory {additional_dir}"
                )
                raise FileNotFoundError
        else:
            found_file_path = find_file_in_directory(ios_repo_dir, filename)
            if not found_file_path:
                logger.error(
                    f"Can not find the file {filename} under the directory {ios_repo_dir}"
                )
                raise FileNotFoundError

        if exists(join(dirpath, filename)):
            logger.info(
                f"The file {filename} already exists under the directory {dirpath}"
            )
        else:
            logger.info(
                f"Found the file: {found_file_path}, copy it to the directory: {dirpath}"
            )
            execute_command(f"cp {found_file_path} {dirpath}", console_output=True)
